﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;
// TT Jul-2008 - NOTE: In July CTP this reference was changed to
// a different DLL -- Dss.Cmd.Args became Microsoft.Dss.Tools
using Microsoft.Dss.Tools.CommandLine;

namespace LineMesh
{

    class LineMesh
    {
        static CommandArguments _parsedArguments;

        static void Main(string[] args)
        {
            try
            {
                _parsedArguments = ParseCommandArgs(args);
                if (_parsedArguments == null)
                {
                    Environment.Exit(CommandExit.UsageError);
                }
                string directory = Path.GetDirectoryName(_parsedArguments.inFilename);
                if (String.IsNullOrEmpty(directory))
                    directory = ".\\";

                string[] matches = Directory.GetFiles(
                    directory,
                    Path.GetFileName(_parsedArguments.inFilename));

                if (matches.Length == 0)
                {
                    ConsoleWriter.WriteError("File does not exist: " + _parsedArguments.inFilename);
                    Environment.Exit(CommandExit.UsageError);
                }

                if ((matches.Length > 1) && (_parsedArguments.outFilename != null))
                {
                    ConsoleWriter.WriteError("Cannot specify an output filename with a wildcard input filename");
                    Environment.Exit(CommandExit.UsageError);
                }

                DateTime BeforeEverything = DateTime.Now.ToLocalTime();

                foreach (string filename in matches)
                    ConvertFile(filename, _parsedArguments.outFilename);

                TimeSpan elapsed = DateTime.Now.ToLocalTime() - BeforeEverything;
                ConsoleWriter.WriteInfo(string.Format("Total time: {0:f1} seconds.", elapsed.TotalSeconds));
            }
            catch (Exception ex)
            {
                ConsoleWriter.WriteError(ex);
                Environment.Exit(CommandExit.InternalError);
            }
        }

        private static void ConvertFile(string srcFile, string destFile)
        {
            Dictionary<string, Color> colors = new Dictionary<string, Color>();

            if (destFile == null)
                destFile = Path.ChangeExtension(srcFile, ".obj");

            string mtlFile = Path.ChangeExtension(destFile, ".mtl");
            TextWriter objWriter = File.CreateText(destFile);
            if (objWriter == null)
            {
                ConsoleWriter.WriteError("Unable to create file " + destFile);
                Environment.Exit(CommandExit.InvalidArgument);
            }
            TextWriter mtlWriter = File.CreateText(mtlFile);
            if (mtlWriter == null)
            {
                ConsoleWriter.WriteError("Unable to create file " + mtlFile);
                Environment.Exit(CommandExit.InvalidArgument);
            }
            
            ConsoleWriter.WriteInfo("Generating " + destFile + " from " + srcFile);

            // load the input file
            Bitmap input = (Bitmap)Bitmap.FromFile(srcFile);
            if (input == null)
            {
                ConsoleWriter.WriteError("Unable to open file " + srcFile);
                Environment.Exit(CommandExit.InvalidArgument);
            }

            float pixelScale;
            if (!Single.TryParse(_parsedArguments.scale, out pixelScale))
            {
                ConsoleWriter.WriteError("Invalid Scale parameter.");
                Environment.Exit(CommandExit.InvalidArgument);
            }

            float parmXOffset;
            if (!Single.TryParse(_parsedArguments.xOffset, out parmXOffset))
            {
                ConsoleWriter.WriteError("Invalid XOffset parameter.");
                Environment.Exit(CommandExit.InvalidArgument);
            }

            float parmZOffset;
            if (!Single.TryParse(_parsedArguments.zOffset, out parmZOffset))
            {
                ConsoleWriter.WriteError("Invalid ZOffset parameter.");
                Environment.Exit(CommandExit.InvalidArgument);
            }

            float parmHeight;
            if (!Single.TryParse(_parsedArguments.height, out parmHeight))
            {
                ConsoleWriter.WriteError("Invalid Height parameter.");
                Environment.Exit(CommandExit.InvalidArgument);
            }

            float pixelSize = 0.1f * pixelScale;
            float xOffset = -input.Width / 2 * pixelSize + parmXOffset;
            float zOffset = -input.Height / 2 * pixelSize + parmZOffset;
            float yOffset = -0.1f * parmHeight / 2;

            objWriter.WriteLine("# Generated from " + srcFile + " by LineMesh.exe.");
            objWriter.WriteLine("mtllib " + Path.GetFileName(mtlFile));
            objWriter.WriteLine("g default");
            objWriter.WriteLine("vt 0.000000 0.000000");
            objWriter.WriteLine("vt 1.000000 0.000000");
            objWriter.WriteLine("vt 0.000000 1.000000");
            objWriter.WriteLine("vt 1.000000 1.000000");
            objWriter.WriteLine("vt 0.000000 2.000000");
            objWriter.WriteLine("vt 1.000000 2.000000");
            objWriter.WriteLine("vt 0.000000 3.000000");
            objWriter.WriteLine("vt 1.000000 3.000000");
            objWriter.WriteLine("vt 0.000000 4.000000");
            objWriter.WriteLine("vt 1.000000 4.000000");
            objWriter.WriteLine("vt 2.000000 0.000000");
            objWriter.WriteLine("vt 2.000000 1.000000");
            objWriter.WriteLine("vt -1.000000 0.000000");
            objWriter.WriteLine("vt -1.000000 1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 1.000000");
            objWriter.WriteLine("vn 0.000000 1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 -1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 -1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 -1.000000");
            objWriter.WriteLine("vn 0.000000 0.000000 -1.000000");
            objWriter.WriteLine("vn 0.000000 -1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 -1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 -1.000000 0.000000");
            objWriter.WriteLine("vn 0.000000 -1.000000 0.000000");
            objWriter.WriteLine("vn 1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn 1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn 1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn 1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn -1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn -1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn -1.000000 0.000000 0.000000");
            objWriter.WriteLine("vn -1.000000 0.000000 0.000000");
            objWriter.WriteLine("s off");
            int vertexOffset = 0;

            for (int z = 0; z < input.Height; z++)
            {
                for (int x = 0; x < input.Width; x++)
                {
                    Color pixel = input.GetPixel(x, z);

                    if ((pixel.R == 0) && (pixel.G == 0) && (pixel.B == 0))
                        continue;

                    string matName = GetMaterialName(pixel);
                    if (!colors.ContainsKey(matName))
                    {
                        colors.Add(matName, pixel);
                        mtlWriter.WriteLine("newmtl " + matName);
                        mtlWriter.WriteLine(string.Format("Kd {0:0.00} {1:0.00} {2:0.00}", pixel.R / 255.0, pixel.G / 255.0, pixel.B / 255.0));
                        mtlWriter.WriteLine("Ka 0.00 0.00 0.00");
                    }

                    // find the largest rectangular area covered by this color
                    int xr = 1;
                    int zr = 1;
                    FindRectangle(x, z, input, out xr, out zr);
                    ClearRectangle(x, z, xr, zr, input);

                    objWriter.WriteLine(string.Format("v {0} {1} {2}",x * pixelSize + xOffset,  yOffset, zr * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",xr * pixelSize + xOffset,  yOffset, zr * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",x * pixelSize + xOffset, -yOffset, zr * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",xr * pixelSize + xOffset, -yOffset, zr * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",x * pixelSize + xOffset, -yOffset, z * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",xr * pixelSize + xOffset, -yOffset, z * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",x * pixelSize + xOffset,  yOffset, z * pixelSize + zOffset));
                    objWriter.WriteLine(string.Format("v {0} {1} {2}",xr * pixelSize + xOffset,  yOffset, z * pixelSize + zOffset));

                    objWriter.WriteLine("usemtl " + matName);
                    objWriter.WriteLine(string.Format("f {0}/1/1 {1}/2/2 {2}/4/3 {3}/3/4", 1 + vertexOffset, 2 + vertexOffset, 4 + vertexOffset, 3 + vertexOffset));
                    objWriter.WriteLine(string.Format("f {0}/3/5 {1}/4/6 {2}/6/7 {3}/5/8", 3 + vertexOffset, 4 + vertexOffset, 6 + vertexOffset, 5 + vertexOffset));
                    objWriter.WriteLine(string.Format("f {0}/5/9 {1}/6/10 {2}/8/11 {3}/7/12", 5 + vertexOffset, 6 + vertexOffset, 8 + vertexOffset, 7 + vertexOffset));
                    objWriter.WriteLine(string.Format("f {0}/7/13 {1}/8/14 {2}/10/15 {3}/9/16", 7 + vertexOffset, 8 + vertexOffset, 2 + vertexOffset, 1 + vertexOffset));
                    objWriter.WriteLine(string.Format("f {0}/2/17 {1}/11/18 {2}/12/19 {3}/4/20", 2 + vertexOffset, 8 + vertexOffset, 6 + vertexOffset, 4 + vertexOffset));
                    objWriter.WriteLine(string.Format("f {0}/13/21 {1}/1/22 {2}/3/23 {3}/14/24", 7 + vertexOffset, 1 + vertexOffset, 3 + vertexOffset, 5 + vertexOffset));
                    vertexOffset += 8;
                }
            }
            objWriter.Close();
            mtlWriter.Close();
        }

        static void ClearRectangle(int xi, int zi, int xr, int zr, Bitmap input)
        {
            for (int x = xi; x < xr; x++)
                for (int z = zi; z < zr; z++)
                    input.SetPixel(x, z, Color.Black);
        }

        static void FindRectangle(int x, int z, Bitmap input, out int xr, out int zr)
        {
            // simple but inefficient algorithm to find a large rectangle of same color
            Color c = input.GetPixel(x, z);
            // find the maximum X range
            int xMax;
            for (xMax = x + 1; xMax < input.Width; xMax++)
                if (input.GetPixel(xMax, z) != c)
                    break;

            // find the maximum Z range
            int zMax;
            for (zMax = z + 1; zMax < input.Height; zMax++)
                if (input.GetPixel(x, zMax) != c)
                    break;

            // pare it down to find a solid rectangle
            if ((zMax - z) > (xMax - x))
            {
                for (int zp = z + 1; zp < zMax; zp++)
                {
                    for (int xp = x + 1; xp < xMax; xp++)
                    {
                        if (input.GetPixel(xp, zp) != c)
                        {
                            xMax = xp;
                        }
                    }
                }
            }
            else
            {
                for (int xp = x + 1; xp < xMax; xp++)
                {
                    for (int zp = z + 1; zp < zMax; zp++)
                    {
                        if (input.GetPixel(xp, zp) != c)
                        {
                            zMax = zp;
                        }
                    }
                }
            }

            xr = xMax;
            zr = zMax;
        }

        static string GetMaterialName(Color c)
        {
            return string.Format("m{0:000}{1:000}{2:000}", c.R, c.G, c.B);
        }

        private static CommandArguments ParseCommandArgs(string[] args)
        {
            CommandArguments parsedArgs = new CommandArguments();
            parsedArgs.inFilename = null;
            parsedArgs.outFilename = null;
            parsedArgs.xOffset = "0";
            parsedArgs.zOffset = "0";
            parsedArgs.scale = "1";
            parsedArgs.height = "1";

            if (!Parser.ParseArgumentsWithUsage(args, parsedArgs))
            {
                return null;
            }

            return parsedArgs;
        }
    }

    class CommandArguments
    {
        internal CommandArguments()
        {
        }

        [Argument(ArgumentType.Required, ShortName = "i", LongName = "input",
            HelpText = "image file to convert to a .obj mesh.  The wildcard characters * and ? are allowed.")]
        public string inFilename;

        [Argument(ArgumentType.AtMostOnce, ShortName = "o", LongName = "output",
            HelpText = "Optional output filename.  A .obj extension is not required.  If this parameter is not specified, the output file will default to the same name as the input file but with a .obj extension.")]
        public string outFilename;

        [Argument(ArgumentType.AtMostOnce, ShortName = "x", LongName = "xOffset",
            HelpText = "Optional X offset for the mesh")]
        public string xOffset;

        [Argument(ArgumentType.AtMostOnce, ShortName = "z", LongName = "zOffset",
            HelpText = "Optional Z offset for the mesh")]
        public string zOffset;

        [Argument(ArgumentType.AtMostOnce, ShortName = "s", LongName = "scale",
            HelpText = "Optional scale multiplier for the mesh.  The default size is 10 cm per pixel.")]
        public string scale;

        [Argument(ArgumentType.AtMostOnce, ShortName = "h", LongName = "height",
            HelpText = "Optional height multiplier for the mesh.  The default height is 10 cm.")]
        public string height;
    }
}
